package com.nulldev.util.web.nTubeExtensions.gvc_spoofer;

import java.util.Map;

import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.maps.sem.SelfExpiringHashMap;
import com.nulldev.util.data.longNumbers.lNumber;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.experiments.Experiments;
import com.nulldev.util.web.HttpServer3.ByteReadRange;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil;
import com.nulldev.util.web.YouTubeExtractor.util.YouTubePOTokenGenerator;

/**
 * GVC (Google Video Client) Query Spoofer
 */
public class GVCSpoofer {

	private static final Logger log = LoggerFactory.getLogger();

	private static final boolean ENABLE_PO_TOKEN = Experiments.get("ntube-gvc://enable-po-token/").isActive();
	private static final boolean ENABLE_SENDING_CVER = Experiments.get("ntube-gvc://enable-cver/").isActive();
	private static final boolean ENABLE_RBUF_IMPLEMENTATION = Experiments.get("ntube-gvc://enable-rbuf/").isActive();
	private static final boolean ENABLE_UMP_POLICY = Experiments.get("ntube-gvc://enable-ump/").isActive();
	private static final boolean ENABLE_SRFVP_POLICY = Experiments.get("ntube-gvc://enable-srfvp/").isActive();
	private static final boolean ENABLE_RN_IMPLEMENTATION = Experiments.get("ntube-gvc://enable-request-number/").isActive();
	private static final boolean ENABLE_SPOOFING_CPN = Experiments.get("ntube-gvc://enable-cpn-spoofing/").isActive();

	private static final boolean FAKE_RBUF_VALUES = Experiments.get("ntube-gvc://gvc-use-fake-values/").isActive();
	private static final RBUFAlgorithm rbuf_algorithm = RBUFAlgorithm.PROPORTIONAL_TO_LENGTH;
	private static final int DEFAULT_RBUF_FAKE_BITRATE = IOUtils.MEMORY_ALLOC_BUFFER * 4;

	private static final int RBUF_FAKE_BITRATE;
	static {
		RBUF_FAKE_BITRATE = DEFAULT_RBUF_FAKE_BITRATE;
	}

	private final static Map<String, lNumber> request_count_cache = new SelfExpiringHashMap<String, lNumber>(300000 /* 5 minutes */, true /* useTrie */);
	private final static Map<String, String> request_cpn_cache = new SelfExpiringHashMap<String, String>(300000 /* 5 minutes */, true /* useTrie */);

	public static String generateSpoofedQueries(final String url, final ByteReadRange[] byteRanges, final boolean privacy_noRequestCountTracking) {
		Variables.requireNonNull(url, "url");

		final StringBuilder spoofed_url = new StringBuilder(url);
		if (ENABLE_PO_TOKEN && !url.contains("&pot=")) {
			try {
				final String po_token = YouTubePOTokenGenerator.makeToken(YouTubeExtractorConstants.VISITOR_DATA);

				spoofed_url.append("&pot=");
				spoofed_url.append(po_token);
			} catch (Throwable tx) {
				log.error("[generateSpoofedQueries] Failed to generate a poToken! [vd=" + YouTubeExtractorConstants.VISITOR_DATA + "]", tx);
			}
		}

		if (ENABLE_SENDING_CVER && !url.contains("&cver=") && YouTubeExtractorConstants.CLIENT_VERSION != null) {
			spoofed_url.append("&cver=");
			spoofed_url.append(YouTubeExtractorConstants.CLIENT_VERSION);
		}

		if (ENABLE_SPOOFING_CPN && !url.contains("&cpn=")) {
			final String id = StringsUtil.substringBetweenO(url, "&id=", "&");
			if (!request_cpn_cache.containsKey(id)) {
				request_cpn_cache.putIfAbsent(id, YouTubeExtractorUtil.makeDummyCPN());
			}
			spoofed_url.append("&cpn=");
			spoofed_url.append(request_cpn_cache.get(id));
		}

		boolean isFirstRequest = false;
		if (ENABLE_RN_IMPLEMENTATION && !privacy_noRequestCountTracking && !url.endsWith(".ts") && !url.contains("&rn=")) {
			final String id = StringsUtil.substringBetweenO(url, "&id=", "&");
			if (request_count_cache.containsKey(id)) {
				final lNumber ccurl = request_count_cache.get(id);
				if (ccurl != null)
					ccurl.addSelf(1);
			} else {
				/* This is correct, DO NOT change. */
				request_count_cache.putIfAbsent(id, lNumber.one());
				isFirstRequest = true;
			}
			spoofed_url.append("&rn=");
			spoofed_url.append(request_count_cache.get(id).toString());
		}

		if (ENABLE_RBUF_IMPLEMENTATION && !url.contains("&rbuf=") && byteRanges != null) {
			final long estimatedBuffer = byteRanges[0].length();
			if (FAKE_RBUF_VALUES) {
				spoofed_url.append("&rbuf=");
				spoofed_url.append(String.format("%04d", RBUF_FAKE_BITRATE));
			} else if (isFirstRequest) {
				spoofed_url.append("&rbuf=0000");
			} else if (estimatedBuffer > 0) {
				final float fmtBuffer;
				switch (rbuf_algorithm) {
					case PROPORTIONAL_TO_LENGTH: {
						final int contentLength = MathUtil.safeParseInt(StringsUtil.substringBetweenO(url, "&clen=", ""), RBUF_FAKE_BITRATE);
						fmtBuffer = ((float) estimatedBuffer / contentLength);
						break;
					}
					case PROPORTIONAL_TO_INITCWNDBPS: {
						final int initcwndbps = MathUtil.safeParseInt(StringsUtil.substringBetweenO(url, "&initcwndbps=", ""), RBUF_FAKE_BITRATE);
						fmtBuffer = ((float) estimatedBuffer / initcwndbps);
						break;
					}
					default:
						throw new UnsupportedOperationException("Unknown algorithm: " + rbuf_algorithm);
				}
				final String trueBuffer = String.format("%04d", (int) (fmtBuffer * 1e4));
				spoofed_url.append("&rbuf=");
				spoofed_url.append(trueBuffer);
			} else {
				log.warn("[RBUF] Invalid estimated buffer: " + estimatedBuffer);
			}
		}

		if (ENABLE_UMP_POLICY) {
			spoofed_url.append("&ump=1");
		}

		if (ENABLE_UMP_POLICY || ENABLE_SRFVP_POLICY) {
			spoofed_url.append("&srfvp=1");
		}
		return spoofed_url.toString();
	}
}
